跳到主要内容

SpringBoot 定时任务

参考资料 廖雪峰的官方网站 使用Scheduler

什么是 Spring Scheduled

在很多应用程序中,经常需要执行定时任务。例如,每天或每月给用户发送账户汇总报表,定期检查并发送系统状态报告,等等。

定时任务在使用线程池一节中已经讲到了,Java标准库本身就提供了定时执行任务的功能。在 Spring 中,使用定时任务更简单,不需要手写线程池相关代码,只需要两个注解即可。

缺点:只适合处理简单的计划任务,不能处理分布式计划任务。在计划任务数量太多的时候,可能出现阻塞,崩溃,延迟启动等问题。 优势:是 Spring 框架提供的计划任务,开发简单,执行效率比较高。且

Scheduled 定时任务是 Spring3.0版本之后自带的一个定时任务,如果要单独使用需要导入依赖

<!-- scheduled所属资源为spring-context-support -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context-support</artifactId>
</dependency>

如果是在 SpringBoot 使用则无需额外的依赖,可以直接在 AppConfig 中加上 @EnableScheduling 就开启了定时任务的支持

基本使用

首先在启动类上加入 @EnableScheduling 注解

@SpringBootApplication
@EnableScheduling
public class AppStarter {

  public static void main(String[] args) {
    SpringApplication.run(AppStarter.class, args);
  }
}

接下来,可以直接在一个 Bean 中编写一个无参数方法,然后加上 @Scheduled 注解:

@Component
public class TaskService {
final Logger logger = LoggerFactory.getLogger(getClass());

@Scheduled(initialDelay = 60_000, fixedRate = 60_000)
public void checkSystemStatusEveryMinute() {
logger.info("Start check system status...");
}
}

上述注解指定了启动延迟60秒,并以60秒的间隔执行任务。现在,我们直接运行应用程序,就可以在控制台看到定时任务打印的日志:

2020-06-03 18:47:32 INFO  [pool-1-thread-1] c.i.learnjava.service.TaskService - Start check system status...
2020-06-03 18:48:32 INFO [pool-1-thread-1] c.i.learnjava.service.TaskService - Start check system status...
2020-06-03 18:49:32 INFO [pool-1-thread-1] c.i.learnjava.service.TaskService - Start check system status...

上面除了可以使用 fixedRate 还可以使用 FixedDelay

FixedRate 是指任务总是以固定时间间隔触发,不管任务执行多长时间

而 FixedDelay 是指,上一次任务执行完毕后,等待固定的时间间隔,再执行下一次任务:

因此,使用 ScheduledThreadPool 时,我们要根据需要选择执行一次、FixedRate 执行还是 FixedDelay 执行。

定时任务的配置放到配置文件

可以把定时任务的配置放到配置文件中,例如 task.properties

task.checkDiskSpace=30000

这样就可以随时修改配置文件而无需动代码。但是在代码中,我们需要用 fixedDelayString 取代 fixedDelay:

@Component
public class TaskService {
...

@Scheduled(initialDelay = 30_000, fixedDelayString = "${task.checkDiskSpace:30000}")
public void checkDiskSpaceEveryMinute() {
logger.info("Start check disk space...");
}
}

注意到上述代码的注解参数 fixedDelayString 是一个属性占位符,并配有默认值 30000,Spring 在处理 @Scheduled 注解时,如果遇到 String,会根据占位符自动用配置项替换,这样就可以灵活地修改定时任务的配置。

此外,fixedDelayString 还可以使用更易读的 Duration,例如:

@Scheduled(initialDelay = 30_000, fixedDelayString = "${task.checkDiskSpace:PT2M30S}")

以字符串 PT2M30S 表示的 Duration 就是2分30秒(Duration 百度)

使用 Cron 任务

还有一类定时任务,它不是简单的重复执行,而是按时间触发,我们把这类任务称为 Cron 任务,例如:

  • 每天凌晨2:15执行报表任务;
  • 每个工作日12:00执行特定任务;
  • ……

Cron源自 Unix/Linux 系统自带的 crond 守护进程,以一个简洁的表达式定义任务触发时间。在 Spring 中,也可以使用 Cron 表达式来执行 Cron 任务,在 Spring 中,它的格式是:

秒 分 小时 天 月份 星期 年

年是可以忽略的,通常不写。每天凌晨 2:15 执行的 Cron 表达式就是:

0 15 2 * * *

每个工作日 12:00 执行的 Cron 表达式就是:

0 0 12 * * MON-FRI

每个月1号,2号,3号和10号12:00执行的 Cron 表达式就是:

0 0 12 1-3,10 * *

在 Spring 中,我们定义一个每天凌晨2:15执行的任务:

@Component
public class TaskService {
...

@Scheduled(cron = "${task.report:0 15 2 * * *}")
public void cronDailyReport() {
logger.info("Start daily report task...");
}
}

Cron 任务同样可以使用属性占位符,这样修改起来更加方便。

Cron 表达式还可以表达每10分钟执行,例如:

0 */10 * * * *

这样,在每个小时的 0:00,10:00,20:00,30:00,40:00,50:00 均会执行任务,实际上它可以取代 fixedRate 类型的定时任务。

让任务并行执行

默认情况下,@Scheduled 任务都在 Spring 创建的大小为1 的默认线程池中执行,可以通过在加了 @Scheduled 注解的方法里加上下面这段代码来验证。

logger.info("Current Thread : {}", Thread.currentThread().getName());

加上上面这段代码的定时任务,每次运行都会输出:

Current Thread : scheduling-1

要让定时任务并行执行有下面两种方式

自定义线程池并行执行

如果需要自定义线程池执行话只需要新加一个实现 SchedulingConfigurer 接口的 configureTasks 的类即可,这个类需要加上 @Configuration 注解。

@Configuration
public class SchedulerConfig implements SchedulingConfigurer {
private final int POOL_SIZE = 10;

@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();

threadPoolTaskScheduler.setPoolSize(POOL_SIZE);
threadPoolTaskScheduler.setThreadNamePrefix("my-scheduled-task-pool-");
threadPoolTaskScheduler.initialize();

scheduledTaskRegistrar.setTaskScheduler(threadPoolTaskScheduler);
}
}

这样再次执行上面的任务输出当前线程的名字会改变。

使用异步来并行执行

如果想要代码并行执行的话,还可以通过 @EnableAsync@Async 这两个注解实现

@Component
@EnableAsync
public class AsyncScheduledTasks {
private static final Logger log = LoggerFactory.getLogger(AsyncScheduledTasks.class);
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

/**
* fixedDelay:固定延迟执行。距离上一次调用成功后2秒才执。
*/
//@Async
@Scheduled(fixedDelay = 2000)
public void reportCurrentTimeWithFixedDelay() {
try {
TimeUnit.SECONDS.sleep(3);
log.info("Fixed Delay Task : The time is now {}", dateFormat.format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

运行程序输出如下,reportCurrentTimeWithFixedDelay() 方法会每5秒执行一次,因为 @Scheduled 任务都在 Spring 创建的大小为1 的默认线程池中执行。

Current Thread : scheduling-1
Fixed Delay Task : The time is now 14:24:23
Current Thread : scheduling-1
Fixed Delay Task : The time is now 14:24:28
Current Thread : scheduling-1
Fixed Delay Task : The time is now 14:24:33

reportCurrentTimeWithFixedDelay() 方法上的 @Async 注解取消注释后输出如下,reportCurrentTimeWithFixedDelay() 方法会每 2 秒执行一次。

Current Thread : task-1
Fixed Delay Task : The time is now 14:27:32
Current Thread : task-2
Fixed Delay Task : The time is now 14:27:34
Current Thread : task-3
Fixed Delay Task : The time is now 14:27:36

创建一个 scheduled task 示例

这里有个 在线生成 scheduled task 的网站

@Component
public class ScheduledTasks {
private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

/**
* fixedRate:固定速率执行。每5秒执行一次。
*/
@Scheduled(fixedRate = 5000)
public void reportCurrentTimeWithFixedRate() {
log.info("Current Thread : {}", Thread.currentThread().getName());
log.info("Fixed Rate Task : The time is now {}", dateFormat.format(new Date()));
}

/**
* fixedDelay:固定延迟执行。距离上一次调用成功后2秒才执。
*/
@Scheduled(fixedDelay = 2000)
public void reportCurrentTimeWithFixedDelay() {
try {
TimeUnit.SECONDS.sleep(3);
log.info("Fixed Delay Task : The time is now {}", dateFormat.format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}

/**
* initialDelay:初始延迟。任务的第一次执行将延迟5秒,然后将以5秒的固定间隔执行。
*/
@Scheduled(initialDelay = 5000, fixedRate = 5000)
public void reportCurrentTimeWithInitialDelay() {
log.info("Fixed Rate Task with Initial Delay : The time is now {}", dateFormat.format(new Date()));
}

/**
* cron:使用Cron表达式。 每分钟的1,2秒运行
*/
@Scheduled(cron = "1-2 * * * * ? ")
public void reportCurrentTimeWithCronExpression() {
log.info("Cron Expression: The time is now {}", dateFormat.format(new Date()));
}
}